package com.hls.mvc.servlet;

import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import javax.servlet.RequestDispatcher;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import com.hls.mvc.annotation.Controller;
import com.hls.mvc.annotation.RequestMapping;
import com.hls.mvc.core.ConfigReader;
import com.hls.mvc.core.PackageUtil;
import com.hls.mvc.core.SpringMappings;
import com.hls.mvc.servlet.view.InternalResourceViewResolver;
import com.hls.mvc.core.Model;

/**
 * Servlet implementation class DispatcherServlet
 */

public class DispatcherServlet extends HttpServlet {
    private static final long serialVersionUID = 1L;
    // requestPath mapping to class and method
    private static Map<String,List<String>> classMapping = null;
    private String configFile;

    
    /**
     * @see HttpServlet#HttpServlet()
     */
    public DispatcherServlet() {
        super();
        // TODO Auto-generated constructor stub
    }
    
    @Override
    public void init() throws ServletException {

        super.init();
        // start to initialize
        System.out.println("start initialize.");
        configFile = this.getInitParameter("contextConfigLocation");
        if (configFile.contains("classpath:")) {
            configFile = configFile.replace("classpath:", "WEB-INF\\classes\\");
        } else {
            configFile = "WEB-INF\\" + configFile;
        }

        // start to read the configuration file
        String serverpath = this.getServletContext().getRealPath("\\");
        SpringMappings springMappings = ConfigReader.configure(serverpath + configFile);
        
        this.getServletContext().setAttribute("springconfig", springMappings);
    }

    

    /**
     * @see HttpServlet#doGet(HttpServletRequest request, HttpServletResponse
     *      response)
     */
    protected void doGet(HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {
        // get configuration
        SpringMappings springMappings = (SpringMappings) this.getServletContext().getAttribute("springconfig");
        // find the page.
        InternalResourceViewResolver irvr = new InternalResourceViewResolver(springMappings.getPrefix(), springMappings.getSuffix());
        // redirect
        String requestPath = request.getRequestURI().replace(this.getServletContext().getContextPath(), "");
        
        // servletMappings only initialize once
        if(classMapping == null)
        {
            classMapping = new HashMap<String,List<String>>();
            // start
            // mapping
            // get Controller mappings
            if (springMappings.getBasePackage() != null) {
                String packageName = springMappings.getBasePackage();
                try {
                    List<String> classNames;
                    if (springMappings.getIncludeSub()) {
                        classNames = PackageUtil.getClassName(packageName, true);
                    } else {
                        classNames = PackageUtil.getClassName(packageName, false);
                    }
                    if (classNames != null) {
                        for (String className : classNames) {
                            if(Class.forName(className).isAnnotationPresent(Controller.class))
                            {
                                initMapping(springMappings.getMappings(), classMapping, className, request, response);
                            }
                        }
                    }
                } catch (Exception e) {
                    e.printStackTrace();
                    System.out.println("error when initializing mappings.");
                }
                
            }
        }
        else
        {
            // Map<String,String> map = springMappings.getMappings();
            requestPath = requestPath.split("\\.")[0];
            List<String> prop = classMapping.get(requestPath);

            try {
                
                if(prop!=null)
                {
                    invokeMethod(prop, request, response);
                }
                
            } catch(Exception e) {
                e.printStackTrace();
                System.out.println("error when finding mappings.");
            }
        }
       
        RequestDispatcher dis = request.getRequestDispatcher(irvr.getJspLocation(requestPath,springMappings.getMappings()));
        dis.forward(request, response);
    }

    /**
     * @see HttpServlet#doPost(HttpServletRequest request, HttpServletResponse
     *      response)
     */
    protected void doPost(HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {
        // TODO Auto-generated method stub
        doGet(request, response);
    }
    
    /**
     * initialize mappings once
     * @param mappings
     * @param className
     * @throws ClassNotFoundException 
     * @throws InvocationTargetException 
     * @throws IllegalArgumentException 
     * @throws IllegalAccessException 
     * @throws InstantiationException 
     */
    private void initMapping(Map mappings,Map clsMapping, String className ,HttpServletRequest request, HttpServletResponse response) throws ClassNotFoundException, IllegalAccessException, IllegalArgumentException, InvocationTargetException, InstantiationException {
       
        String rootPath = "";
        Class controller = Class.forName(className);
        Object controllerObj = controller.newInstance();
        if(controller.isAnnotationPresent(RequestMapping.class))
        {
            RequestMapping requestMapping = (RequestMapping) controller.getAnnotation(RequestMapping.class);
            if(requestMapping.value().length()>0)
            {
                rootPath = requestMapping.value();
            }
            
            Method[] methods = controller.getMethods();
            //use this map to manage request path and real path
            for(Method method : methods)
            {
                String view = "";
                if(method.isAnnotationPresent(RequestMapping.class))
                {
                    RequestMapping subRequestMapping = (RequestMapping) method.getAnnotation(RequestMapping.class);
                    if(subRequestMapping.value().length()>0)
                    {
                        //System.out.println("method name="+method.getName());
                        String path = rootPath+subRequestMapping.value();
                        
                        int paraCount = method.getParameterCount();
                        if(paraCount == 0)
                        {
                            view = (String) method.invoke(controllerObj, new Class[]{});
                        }
                        else if(paraCount == 1 && method.getParameterTypes()[0] == Model.class)
                        {
                            Model model = new Model();
                            view = (String) method.invoke(controllerObj, model);
                            if(model.getAttibutes() != null)
                            {
                                for(String key : model.getAttibutes().keySet())
                                {
                                    request.setAttribute(key, model.getAttibutes().get(key));
                                } 
                            }
                        }
                        else if(paraCount == 2 && method.getParameterTypes()[0]== HttpServletRequest.class && method.getParameterTypes()[1]== HttpServletResponse.class)
                        {
                            view = (String) method.invoke(controllerObj, request, response);
                        }
                        else
                        {
                            view = "";
                        }
                        if(view.length()>0)
                        {
                            mappings.put(path, view);
                            List<String> prop = new ArrayList<String>();
                            prop.add(className);
                            prop.add(method.getName());
                            prop.add(((Integer)method.getParameterCount()).toString());
                            clsMapping.put(path, prop);
                        }
                    }
                }
            }
        }
    }

    /**
     * use reflection to invoke method.
     */
    private void invokeMethod(List<String> prop,HttpServletRequest request, HttpServletResponse response) throws ClassNotFoundException, IllegalAccessException, IllegalArgumentException, InvocationTargetException, InstantiationException {
        String className = prop.get(0);
        String medhodName = prop.get(1);
        int paraCount = Integer.parseInt(prop.get(2));

        Class controller = Class.forName(className);
        Object controllerObj = controller.newInstance();
         
        Method[] methods = controller.getMethods();
        // use this map to manage request path and real path
        for (Method method : methods) {
            if(method.getName().equals(medhodName))
            {
                if (paraCount == 0) {
                    method.invoke(controllerObj, new Class[] {});
                }
                else if (paraCount == 1 && method.getParameterTypes()[0] == Model.class) {
                    Model model = new Model();
                    method.invoke(controllerObj, model);
                    if (model.getAttibutes() != null) {
                        for (String key : model.getAttibutes().keySet()) {
                            request.setAttribute(key, model.getAttibutes().get(key));
                        }
                    }
                }
                else if (paraCount == 2 && method.getParameterTypes()[0] == HttpServletRequest.class
                        && method.getParameterTypes()[1] == HttpServletResponse.class) {
                    method.invoke(controllerObj, request, response);
                }
            }
        }
    }


}
